// Copyright (c) 2009 - Quildreen <quildreen@gmail.com>                     
//                                                                          
//  Permission is hereby granted, free of charge, to any person              
//  obtaining a copy of this software and associated documentation files     
//  (the "Software"), to deal in the Software without restriction,           
//  including without limitation the rights to use, copy, modify, merge,     
//  publish, distribute, sublicense, and/or sell copies of the Software,     
//  and to permit persons to whom the Software is furnished to do so,        
//  subject to the following conditions:                                     
//                                                                           
//  The above copyright notice and this permission notice shall be           
//  included in all copies or substantial portions of the Software.          
//                                                                           
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,          
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF       
//  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                    
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE   
//  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION   
//  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION    
//  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 

/**
 * lily.graphics.sprite
 *
 * Implements basic and abstract sprite management.
 *
 * ``Sprites`` are, basically, graphical objects that controls their
 * own position in the game's plane -- by such, they hold their position
 * in the global game's plane and draw theirselves at this position.
 *
 * This module attempts to provide abstraction to sprite management in most
 * games -- or in most web games at this time -- by common classes and methods.
 * 
 * Requires: `graphics.collider`
 **/
lily.require("graphics.collider");
lily.onLoad(function() {
    
    lily.defineModule("graphics.sprite");
    

    /**
     * class lily.graphics.sprite.Sprite
     * 
     * Sprites are, basically, graphical objects that controls their
     * own position in the game's plane. They are expected to define a
     * updating method ``update`` and a ``get_rect`` method, which
     * should return the position and dimensions of the Sprite as an
     * array ``[x, y, width, height]``.
     **/
    lily.graphics.sprite.Sprite = Class.create(
    {
        /**
         * new lily.graphics.sprite.Sprite(groups[, options])
         * - groups (Array): array of [[lily.graphics.sprite.SpriteGroup]]
         *   instances that this sprite belongs to.
         * - options (Object): additional options for the sprite object.
         **/
        initialize: function(groups)
        {
            var options = Object.extend({
                x: 0,
                y: 0,
                z: 0,
                width: 0,
                height: 0,
                visible: true,
                solid: true,
                skip_frames: 0,
                force_update: false
            }, arguments[1]);

            /**
             * lily.graphics.sprite.Sprite#id -> Number (read only)
             * 
             * Unique identification for the instance.
             **/
            this.id = lily.uniqueID();

            /**
             * lily.graphics.sprite.Sprite#_group_count -> Number
             * 
             * The number of groups this sprite belongs to.
             **/
            this._group_count = 0;

            /**
             * lily.graphics.sprite.Sprite#_groups -> Object
             * 
             * An object of boolean values that defines which groups this
             * sprite belongs to.
             **/
            this._groups = {};

            /**
             * lily.graphics.sprite.Sprite#_last_update -> Number
             * 
             * How much frames elapsed since this sprite was lastly rendered.
             **/
            this._last_update = 0;

            /**
             * lily.graphics.sprite.Sprite#_oldst -> Date
             * 
             * The time when this sprite was last updated.
             **/
            this._oldst = null;

            /**
             * lily.graphics.sprite.Sprite#x -> Number
             * 
             * The position of the sprite on the game's screen.
             **/
            this.x = options.x;

            /**
             * lily.graphics.sprite.Sprite#y -> Number
             * 
             * The position of the sprite on the game's screen.
             **/
            this.y = options.y;

            /**
             * lily.graphics.sprite.Sprite#z -> Number
             * 
             * The position of the sprite on the game's screen.
             **/
            this.z = options.z;

            /**
             * lily.graphics.sprite.Sprite#width -> Number
             * 
             * The size of the sprite in the screen.
             **/
            this.width = options.width;

            /**
             * lily.graphics.sprite.Sprite#height -> Number
             * 
             * The size of the sprite in the screen.
             **/
            this.height = options.height;

            /**
             * lily.graphics.sprite.Sprite#visible -> Boolean
             * 
             * Whether the sprite is visible on the screen or not.
             **/
            this.visible = options.visible;

            /**
             * lily.graphics.sprite.Sprite#solid -> Boolean
             * 
             * Whether the sprite can collide with other sprites or not.
             **/
            this.solid = options.solid;

            /**
             * lily.graphics.sprite.Sprite#skip_frames -> Number
             * 
             * The number of frames to skip before updating and redrawing this
             * sprite.
             **/
            this.skip_frames = options.skip_frames;

            /**
             * lily.graphics.sprite.Sprite#force_update -> Boolean
             * 
             * Whether the sprite should be forced to be updated on the next game
             * cycle.
             **/
            this.force_update = options.force_update;

            /**
             * lily.graphics.sprite.Sprite#alive -> Boolean
             * 
             * Whether this sprite is alive or not.
             **/
            this.alive = true;


            this.add.apply(this, groups);
        },


        /**
         * lily.graphics.sprite.Sprite#_add(group) -> undefined
         * 
         * Adds a single group to this sprite.
         **/
        _add: function(group)
        {
            this._groups[group.id] = group;
            this._group_count++;
        },


        /**
         * lily.graphics.sprite.Sprite#_remove(group) -> undefined
         * 
         * Removes a single group from this sprite.
         **/
        _remove: function(group)
        {
            delete this._groups[group.id];
            this._group_count--;
        },


        /**
         * lily.graphics.sprite.Sprite#add(groups...) -> undefined
         * 
         * Adds a range of groups to this sprite. The function accepts a range
         * of [[lily.graphics.sprite.SpriteGroup]] instances as positional
         * arguments.
         **/
        add: function()
        {
            for (var i=arguments.length; i--; ) {
                if (!this._groups[arguments[i]]) {
                    this._add(arguments[i]);
                    arguments[i]._add(this);
                }
            }
        },


        /**
         * lily.graphics.sprite.Sprite#remove(groups...) -> undefined
         * 
         * Removes a range of groups from this sprite. The function accepts a
         * range of [[lily.graphics.sprite.SpriteGroup]] instances as
         * positional arguments.
         **/
        remove: function()
        {
            for (var i=arguments.length; i--; ) {
                arguments[i]._remove(this);
                this._remove(arguments[i]);
            }
        },


        /**
         * lily.graphics.sprite.Sprite#render(lag) -> undefined
         * 
         * Renders the sprite to the screen.
         **/
        render: function(lag){
            /* void */
        },


        /**
         * lily.graphics.sprite.Sprite#update(lag) -> undefined
         * 
         * Updates this sprite. Derived classes are to rewrite this function
         * implementing the actual sprite logic.
         **/
        update: function(lag){
            var st = new Date();
            this._oldst || (this._oldst = st);
            lag = st - this._oldst;
            this._oldst = st;
            this._last_update = 0;

            if (this.visible) {
                this.render(lag);
            }
        },


        /**
         * lily.graphics.sprite.Sprite#getRect() -> Object
         * 
         * Returns the sprite's bounding rectangle.
         **/
        getRect: function()
        {
            return {
                left: this.x,
                top: this.y,
                right: this.x + this.width,
                bottom: this.y + this.height
            };
        },


        /**
         * lily.graphics.sprite.Sprite#die() -> undefined
         * 
         * Kills the sprite removing it from any groups it belongs to.
         **/
        die: function()
        {
            this.remove.apply(this, $H(this._groups).values());
            this.alive = false;
        }
    });


    /**
     * class lily.graphics.sprite.SpriteGroup
     * 
     * Implements basic management of sprites.
     **/
    lily.graphics.sprite.SpriteGroup = Class.create(
    {
        /**
         * new lily.graphics.sprite.SpriteGroup(sprites)
         * - sprites (Array): array of [[lily.graphics.sprite.Sprite]] instances.
         **/
        initialize: function(sprites)
        {
            /**
             * lily.graphics.sprite.SpriteGroup#id -> Number (read only)
             * 
             * Unique identifier for the instance.
             **/
            this.id = lily.uniqueID();

            /**
             * lily.graphics.sprite.SpriteGroup#_sprites -> Object
             * 
             * A dictionary of boolean values that defines which sprites this
             * group owns.
             **/
            this._sprites = [];


            this.add.apply(this, sprites);
        },


        /**
         * lily.graphics.sprite.SpriteGroup#_add(sprite) -> undefined
         * 
         * Adds a single sprite to this group.
         **/
        _add: function(sprite)
        {
            this._sprites.push(sprite);
        },


        /**
         * lily.graphics.sprite.SpriteGroup#_remove(sprite) -> undefined
         * 
         * Removes a single sprite from this group.
         **/
        _remove: function(sprite)
        {
            var p = this._sprites.indexOf(sprite);
            if (p) {
                this._sprites.splice(p, 1);
            }
        },


        /**
         * lily.graphics.sprite.SpriteGroup#add(sprites...) -> undefined
         * 
         * Adds a range of sprites to this group. The function accepts a range
         * of positional [[lily.graphics.sprite.Sprite]] instances as
         * positional arguments.
         **/
        add: function()
        {
            for (var i=arguments.length; i--; ) {
                this._add(arguments[i]);
                arguments[i]._add(this);
            }
        },


        /**
         * lily.graphics.sprite.SpriteGroup#remove(sprites...) -> undefined
         * 
         * Removes a range of sprites from this group. The function accepts a
         * range of positional [[lily.graphics.sprite.Sprite]] instances as
         * positional arguments.
         **/
        remove: function()
        {
            for (var i=arguments.length; i--; ) {
                this._remove(arguments[i]);
                arguments[i]._remove(this);
            }
        },


        /**
         * lily.graphics.sprite.SpriteGroup#empty() -> undefined
         * 
         * Removes all sprites from this group.
         **/
        empty: function()
        {
            var sprites = this._sprites;

            for (var i=sprites.length; i--; ) {
                sprite._remove(this);
            }

            this._sprites = [];
        },


        /**
         * lily.graphics.sprite.SpriteGroup#has(sprites...) -> Boolean
         * 
         * Tests if this groups contains at least one of the given sprites. The
         * function accepts a range of [[lily.graphics.sprite.Sprite]] as
         * positional arguments and will return as soon as it finds one that
         * belongs in this group.
         **/
        has: function()
        {
            for (var i=arguments.length; i--; ) {
                if (this._sprites.indexOf(arguments[i])) {
                    return true;
                }
            }

            return false;
        },


        /**
         * lily.graphics.sprite.SpriteGroup#clone() -> lily.graphics.sprite.SpriteGroup
         * 
         * Makes a shallow copy of this group. This will instanciate a new
         * group and insert all of this group's members on it.
         **/
        clone: function()
        {
            return new lily.sprite.SpriteGroup(this._sprites);
        },


        /**
         * lily.graphics.sprite.SpriteGroup#update(lag) -> undefined
         * 
         * Updates this group and all of its members. This will call update on all
         * sprites belonging to this group.
         **/
        update: function(lag)
        {
            var sprites = this._sprites;

            for (var i=sprites.length; i--; ) {
                var sprite = sprites[i];

                if (sprite.alive && ((sprite.force_update || !sprite.skip_frames)
                   || (sprite._last_update >= sprite.skip_frames))) {
                    sprite.update(lag);
                    sprite._last_update = 0;
                }
                ++sprite._last_update;
            }
        },


        /**
         * lily.graphics.sprite.SpriteGroup.collide(sprites[, kill_members][, kill_sprites][, test_only]) -> Boolean | Array
         * - sprites (Array | SpriteGroup): sprites to test collisions with.
         * - kill_members (Boolean): whether to kill sprites in this group if
         *   they collide.
         * - kill_sprites (Boolean): whether to kill any of the given sprites
         *   if they collide.
         * - test_only (Boolean): whether to just test for collision or catch
         *   all the colliding sprites.
         * 
         * Test if any of the members of this group collide with the given sprites
         * or sprite groups.
         *
         * It returns, by default, a boolean value indicating whether or not
         * any of the sprites collided. But if `test_only` is false it'll
         * return an array of all the given sprites that are intersecting each
         * other.
         **/
        collide: function(sprites, kill_members, kill_sprites, test_only)
        {
            var _own_sprites = this._sprites,
                _own_rects = [],
                len = _own_sprites.length,
                boxCollide = lily.graphics.collider.boundingBox,
                rv = [],

                i, j, /* iteration variables */

                s, s2, rect1, rect2;


            if (sprites._sprites) {
                sprites = sprites._sprites;
            }

            if (test_only !== false) {
                test_only = true;
                rv = false;
            }

            for (i = sprites.length; i--; ) {
                s = sprites[i];
                rect1 = sprites[i].getRect();

                if (s.alive && s.solid) {
                     for (j = len; j--; ) {
                        s2 = _own_sprites[j];
                        if (s2.alive && s.solid
                           && boxCollide(rect1, _own_rects[j]
                           || (_own_rects[j] = s2.getRect()))) {

                            if (kill_members || kill_sprites) {
                                kill_members && s2.die();
                                kill_sprites && s.die();
                            } else if(test_only) {
                                return true;
                            }

                            if (test_only) {
                                rv = true;
                            } else {
                                rv.push.apply(rv, [s, s2]);
                            }
                        }
                    }
                }
            }

            return rv;
        }
    });

}); // onLoad